package satk.database;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Clase para controlar la base de datos
 * @author Sergio Flores Genis <http://sergenis.x10.mx>, 16/04/2011
 */
public class DBManager {

    /** Conexión a la base de datos */
    private java.sql.Connection conexion;
    private boolean externalConnection = false;
    private final String dataBase = "jdbc:mysql://localhost/satkdatabase";
    private final String user = "root";
    private final String pass = "r00t";

    /**
     * Constructor para una nueva instancia basada en una conexión existente
     * @param conexion Conexion exitente a la base de datos
     */
    public DBManager(java.sql.Connection conexion) {
        this.conexion = conexion;
        this.externalConnection = true;
    }

    /**
     * Contructor de una instancia de conexión a la base de datos, 
     * en la cronstrucción se abre ina nueva conexión.
     */
    public DBManager() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            this.conexion = java.sql.DriverManager.getConnection(dataBase, user, pass);
        } catch (ClassNotFoundException ex) {
            satk.tools.GeneralEx.excepcionClassNotFoundException(DBManager.class.getName(), ex);
        } catch (SQLException ex) {
            Logger.getLogger(DBManager.class.getName()).log(Level.SEVERE,
                    "Error al conectarse a la base de datos", ex);

        }
    }

    /**
     * Obtiene la conexión a actual a la base de datos
     * @return Conexnión actual a la base de datos
     */
    public java.sql.Connection getConexion() {
        return conexion;
    }

    private void startConnection() throws SQLException {
        if (this.conexion == null || this.conexion.isClosed()) {
            this.conexion = java.sql.DriverManager.getConnection(dataBase, user, pass);
        }
    }

    /** Cierra la conección a la base de datos. */
    public void closeConexion() {
        if (!externalConnection) {
            try {
                if (this.conexion != null && !this.conexion.isClosed()) {
                    this.conexion.close();
                }
            } catch (SQLException ex) {
                Logger.getLogger(DBManager.class.getName()).log(Level.SEVERE,
                        "Error al cerrar la conexion con la base de datos", ex);
            }
        }
    }

    /**
     * Realiza una consulta (select) en la base de datos
     * @param query SQL Script que se ejecutara (tiene que ser de tipo SELECT).
     * @return Resultado de la consulta
     * @throws SQLException Cuando ocurre un error al realizar la consulta
     */
    public ResultSet select(String query) throws SQLException {
        startConnection();
        Statement sm = conexion.createStatement();
        return sm.executeQuery(query);
    }

    /**
     * Realiza una consulta (select) en la base de datos 
     * llenando los parametros enviandos
     * @param query Script que se ejecuta (select)
     * @param elms Parametros que se llenaran antes de la consulta (?)
     * @return Resultado de la consulta
     * @throws SQLException  Cuando ocura un erroa el realizar la consulta
     */
    public ResultSet select(String query, Prepare elms) throws SQLException {
        startConnection();
        PreparedStatement pstm = conexion.prepareStatement(query);
        int i = 1;
        for (Prepare.Entry e : elms) {
            pstm.setObject(i, e.getValue(), e.getKey());
            i++;
        }
        return pstm.executeQuery();
    }

    public ResultSet find(String table, String columnWhere, String value, int limit) throws SQLException {
        startConnection();
        String query = "SELECT * FROM " + table + " WHERE " + columnWhere + " LIKE ? ";
        query += (limit > 0) ? (" LIMIT " + limit + ";") : ";";
        PreparedStatement pstm = conexion.prepareStatement(query);
        pstm.setString(1, value);
        return pstm.executeQuery();
    }

    public ResultSet find(String table, java.util.Map<String, Prepare.Entry> colmunValueFind, int limit) throws SQLException {
        startConnection();
        StringBuilder query = new StringBuilder("SELECT * FROM " + table + "WHERE ");

        for (int i = 0; i < colmunValueFind.size(); i++) {
            if (i > 0) {
                query.append(" AND");
            }
            query.append("? LIKE ?");
        }
        query.append((limit > 0) ? (" LIMIT " + limit + ";") : ";");

        int numP = 1;
        PreparedStatement pstm = conexion.prepareStatement(query.toString());
        for (Map.Entry<String, Prepare.Entry> entry : colmunValueFind.entrySet()) {
            pstm.setObject(numP++, entry.getKey(), Types.VARCHAR);
            pstm.setObject(numP++, entry.getValue().getValue(), entry.getValue().getKey());
        }
        return pstm.executeQuery();
    }

    /**
     * Realiza una inscerción, actualización o eliminación en la base de datos.
     * @param query SQL Script que se ejecuta (INSERT, UPDATE, DELETE)
     * @param elms Matriz con lo elementos para rellenar los argumentos del script
     * @return Resultado de la ejecución
     * @throws SQLException  Cuando ocurre un error al actualizar la información
     */
    public int edit(String query, Prepare elms) throws SQLException {
        startConnection();
        PreparedStatement pstm = conexion.prepareStatement(query);
        int i = 1;
        for (Prepare.Entry e : elms) {
            pstm.setObject(i, e.getValue(), e.getKey());
            i++;
        }
        return pstm.executeUpdate();
    }

    /**
     * Obtiene el ultimo identificador de la opereación <em>INSERT</em> realizada
     * @return Identificador de la iltima opereación de <em>INSERT</em>
     * @throws SQLException  Cuando ocurre un error al actualizar la información
     */
    public int getLastIdInsert() throws SQLException {
        PreparedStatement pstm = conexion.prepareStatement("SELECT @@identity AS id;");
        ResultSet r = pstm.executeQuery();
        r.first();
        return r.getInt(1);
    }

    public void prepareUpdate(StringBuilder sb, String colum, Prepare prepare, Prepare.Entry entry) {
        if (prepare.size() > 0) {
            sb.append(String.format(", %s = ?", colum));
        } else {
            sb.append(String.format("SET %s = ?", colum));
        }
        prepare.add(entry);
    }

    /**
     * Clase para preparar los argumentos y elementos para un SQL UPDATE
     */
    public static class PrepareUpdate {

        private StringBuilder set = null;
        private Prepare prepare = null;

        public PrepareUpdate() {
            this.set = new StringBuilder();
            this.prepare = new Prepare();
        }

        public PrepareUpdate(int initialCapacity) {
            this.set = new StringBuilder();
            this.prepare = new Prepare(initialCapacity);
        }

        public Prepare getPrepare() {
            return prepare;
        }

        public String getSet() {
            return set.toString();
        }

        public boolean add(String column, Prepare.Entry entry) {
            if (this.prepare.add(entry)) {
                putColumn(column);
                return true;
            }
            return false;
        }

        public boolean add(String colum, Object o, int t) {
            if (this.prepare.add(o, t)) {
                putColumn(colum);
                return true;
            }
            return false;
        }

        public boolean add(String colum, Object o, int t, int StringCase) {
            if (this.prepare.add(o, t, StringCase)) {
                putColumn(colum);
                return true;
            }
            return false;
        }

        private void putColumn(String colum) {
            if (this.prepare.size() > 1) {
                this.set.append(String.format(", %s = ?", colum));
            } else {
                this.set.append(String.format("SET %s = ?", colum));
            }
        }
    }

    /**
     * Clase para preparar argumentos de un script SQL
     */
    public static class Prepare extends ArrayList<Prepare.Entry> {

        /** Indica que se mantendra el texto tal y como esta */
        public static final int STRING_DEFAULT = 0;
        /** Indica que el texto sera totalmente en mayusculas <strong>(DEFAULT)</strong> */
        public static final int STRING_UPERCASE = 1;
        /** Indica que el texto tendra la primeraletra de cada palabra en mayusculas */
        public static final int STRING_CAPITAL = 2;
        /** Indica que el texto sera totalmente en minusculas */
        public static final int STRING_LOWERCASE = 3;
        private int stringTransform = 1;

        public Prepare() {
            super();
        }

        public Prepare(int initialCapacity) {
            super(initialCapacity);
        }

        /**
         * Establece el criterio de transformación del texto (STRING_UPERCASE predeterminado)
         * @param stringTransform Criterio de transformación del texto
         */
        public void setStringTransform(int stringTransform) {
            this.stringTransform = stringTransform;
        }

        /**
         * Agrega un nuevo parametro a la colección, El orden en que se ingresan
         * es el orden en el que se procesan.
         * @param o Objeto o dato que se agrega
         * @param t Tipo de dato de la base de datos.
         * @return TRUE: si se agrego corecctamente
         * @see java.sql.Types
         */
        public boolean add(Object o, int t) {
            if (o instanceof String && !String.valueOf(o).isEmpty()) {
                o = this.formateText(String.valueOf(o), stringTransform);
            }
            Prepare.Entry entry = new Prepare.Entry(t, o);
            return (super.add(entry));
        }

        public boolean add(Object o, int t, int StringCase) {
            if (o instanceof String && !String.valueOf(o).isEmpty()) {
                o = this.formateText(String.valueOf(o), StringCase);
            }
            Prepare.Entry entry = new Prepare.Entry(t, o);
            return (super.add(entry));
        }

        private String formateText(String s, int stringCase) {
            switch (stringCase) {
                case STRING_CAPITAL:
                    return s.toUpperCase();
                case STRING_UPERCASE:
                    return s.toUpperCase();
                case STRING_LOWERCASE:
                    return s.toLowerCase();
                default:
                    return s;
            }
        }

        @Override
        public String toString() {
            return String.format("PREPARE@%d", size());
        }

        /**
         * Clase para omacenar la información de un parametro para un script SQL         
         */
        public static class Entry implements Map.Entry<Integer, Object> {

            /** Tipo de dato SQL que es el objeto */
            private Integer key;
            /** Objeto que se procesa */
            private Object value;

            /**
             * Constructor para un nuevo parametro
             * @param key Tipo de dato que es el objeto (java.sql.Types)
             * @param value  Objeto que se procesa
             * @see java.sql.Types
             */
            public Entry(Integer key, Object value) {
                this.key = key;
                this.value = value;
            }

            @Override
            /**
             * Obtiene en tipo de dato que es el objeto
             * @return Tipo de dato, (java.sql.Types)
             */
            public Integer getKey() {
                return key;
            }

            @Override
            /**
             * Obtiene el objeto que se procesara
             * @return Objeto a procesar
             */
            public Object getValue() {
                return value;
            }

            @Override
            public Object setValue(Object value) {
                Object oldValue = this.value;
                if (!oldValue.equals(value)) {
                    this.value = value;
                }
                return oldValue;
            }

            @Override
            public String toString() {
                return String.format("Entry@%s=%s", key, value);
            }
        } // En Prepare.Entry
    } // En Prepare
}
